package com.myrslok.util;

import com.myrslok.set.FileToFileResource;
import com.myrslok.set.SourceTargetResource;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

public class ZipUtil {

    private static final Logger logger = LoggerFactory.getLogger(ZipUtil.class);

    private static final int STREAM_BUFFER_SIZE = 2048;
    private static final int ZIP_COMPRESSION_LEVEL = 9;

    private ZipUtil() {
    }

    public static ZipResult zip(@NotNull final File target, @NotNull final Set<FileToFileResource> resources) {
        createTargetDir(target);
        final ZipResult result = new ZipResult();
        try {
            final FileOutputStream fos = new FileOutputStream(target, false);
            final ZipOutputStream zip = new ZipOutputStream(fos);
            zip.setLevel(ZIP_COMPRESSION_LEVEL);
            zip.setMethod(JarOutputStream.DEFLATED);
            for (@NotNull final FileToFileResource fileResource : resources) {
                final ZipEntry entry = makeZipEntry(fileResource);
                writeEntry(result, zip, fileResource, entry);
            }
            zip.finish();
            zip.close();
        } catch (ZipException e) {
            // todo java.util.zip.ZipException: ZIP file must have at least one entry
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return result;
    }

    public static ZipResult jar(@NotNull final File target, @NotNull final Set<FileToFileResource> set, @Nullable final Manifest manifest) {
        createTargetDir(target);
        final ZipResult result = new ZipResult();
        try {
            final FileOutputStream fos = new FileOutputStream(target, false);
            final JarOutputStream jar;
            if (manifest != null) {
                jar = new JarOutputStream(fos, manifest);
            } else {
                jar = new JarOutputStream(fos);
            }
            jar.setLevel(ZIP_COMPRESSION_LEVEL);
            jar.setMethod(JarOutputStream.DEFLATED);
            for (@NotNull final FileToFileResource fileResource : set) {
                final JarEntry entry = makeJarEntry(fileResource);
                writeEntry(result, jar, fileResource, entry);
            }
            jar.finish();
            jar.close();
        } catch (ZipException e) {
            // todo java.util.zip.ZipException: ZIP file must have at least one entry
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return result;
    }

    private static void createTargetDir(@NotNull final File target) {
        final File parentFile = target.getParentFile();
        if (parentFile.mkdirs()) {
            logger.info("Created dir {}", parentFile);
        }
    }

    /**
     * Fixes the problems documented at http://stackoverflow.com/questions/1281229/how-to-use-jaroutputstream-to-create-a-jar-file
     * @param fileResource
     * @return
     */
    private static JarEntry makeJarEntry(@NotNull final SourceTargetResource<File, File> fileResource) {
        final File file = fileResource.getTarget();
        final JarEntry entry;
        if (file.isDirectory()) {
            entry = makeDirJarEntry(file);
        }  else {
            entry = makeFileJarEntry(file);
        }
        entry.setTime(file.lastModified());
        return entry;
    }

    private static JarEntry makeFileJarEntry(final File file) {
        final String name = replaceBackSlashes(file);
        assert !name.endsWith("/");
        return new JarEntry(name);
    }

    private static JarEntry makeDirJarEntry(@NotNull final File file) {
        String name = replaceBackSlashes(file);
        if (!name.isEmpty()) {
            if (!name.endsWith("/")) {
                name += "/";
            }
        }
        return new JarEntry(name);
    }

    /**
     * Fixes the problems documented at http://stackoverflow.com/questions/1281229/how-to-use-jaroutputstream-to-create-a-jar-file
     * @param fileResource
     * @return
     */
    private static ZipEntry makeZipEntry(@NotNull final SourceTargetResource<File, File> fileResource) {
        final File file = fileResource.getTarget();
        final ZipEntry entry;
        if (file.isDirectory()) {
            entry = makeDirZipEntry(file);
        }  else {
            entry = makeFileZipEntry(file);
        }
        entry.setTime(file.lastModified());
        return entry;
    }

    private static ZipEntry makeFileZipEntry(final File file) {
        final String name = replaceBackSlashes(file);
        assert !name.endsWith("/");
        return new JarEntry(name);
    }

    private static ZipEntry makeDirZipEntry(@NotNull final File file) {
        String name = replaceBackSlashes(file);
        if (!name.isEmpty()) {
            if (!name.endsWith("/")) {
                name += "/";
            }
        }
        return new ZipEntry(name);
    }

    private static String replaceBackSlashes(final File file) {
        return file.getPath().replace("\\", "/");
    }

    private static void writeEntry(@NotNull final ZipResult result, @NotNull final ZipOutputStream zip, @NotNull final SourceTargetResource<File, File> fileResource, @NotNull final ZipEntry entry) throws IOException {
        final File sourceFile = fileResource.getSource();
        entry.setTime(sourceFile.lastModified());
        final FileInputStream fis = new FileInputStream(sourceFile);
        try {
            zip.putNextEntry(entry);
            writeToStream(zip, fis);
            zip.closeEntry();
        } catch (SecurityException e) {
            throw new RuntimeException("Read denied for " + sourceFile, e);
        } catch (ZipException e) {
            throw new RuntimeException("Zip format error during " + entry, e);
        } catch (FileNotFoundException e) {
            result.addMissingFile(fileResource.getSource());
        }
        finally {
            if (fis != null) {
                fis.close();
            }
        }
    }

    private static void writeToStream(@NotNull final OutputStream out, @NotNull final FileInputStream in) throws IOException {
        final byte[] buf = new byte[STREAM_BUFFER_SIZE];
        int len;
        while ((len = in.read(buf)) != -1) {
            out.write(buf, 0, len);
        }
        in.close();
    }

    public static void unjar(@NotNull final File file, @NotNull final File toDir) {
        try {
            final JarFile jarFile = new JarFile(file);

            final Enumeration jarFileEntries = jarFile.entries();

            // Process each entry
            while (jarFileEntries.hasMoreElements()) {
                // grab a zip file entry
                final JarEntry entry = (JarEntry) jarFileEntries.nextElement();
                unzip(entry, toDir, jarFile.getInputStream(entry));
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void unzip(@NotNull final File file, @NotNull final File toDir) {
        try {
            final ZipFile zipFile = new ZipFile(file);

            final Enumeration zipFileEntries = zipFile.entries();

            // Process each entry
            while (zipFileEntries.hasMoreElements()) {
                // grab a zip file entry
                final ZipEntry entry = (ZipEntry) zipFileEntries.nextElement();
                unzip(entry, toDir, zipFile.getInputStream(entry));
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private static void unzip(@NotNull final ZipEntry entry, @NotNull final File toDir, @NotNull final InputStream zipFileInputStream) throws IOException {
        final String currentEntry = entry.getName();
        final File destFile = new File(toDir, currentEntry);
        final File destinationParent = destFile.getParentFile();

        // create the parent directory structure if needed
        destinationParent.mkdirs();

        if (!entry.isDirectory()) {
            final BufferedInputStream is = new BufferedInputStream(zipFileInputStream);
            int currentByte;
            // establish buffer for writing file
            final byte[] data = new byte[STREAM_BUFFER_SIZE];

            // write the current file to disk
            final FileOutputStream fos = new FileOutputStream(destFile);
            final BufferedOutputStream dest = new BufferedOutputStream(fos, STREAM_BUFFER_SIZE);

            // read and write until last byte is encountered
            while ((currentByte = is.read(data, 0, STREAM_BUFFER_SIZE)) != -1) {
                dest.write(data, 0, currentByte);
            }
            dest.flush();
            dest.close();
            is.close();
        }
    }


    public static class ZipResult {
        private final Set<File> missingFiles = new HashSet<File>();

        public void addMissingFile(@NotNull final File file) {
            missingFiles.add(file);
        }

        public Set<File> getMissingFiles() {
            return missingFiles;
        }

    }

}
